import { CollectionSignal } from './CollectionSignal.js';
import {
  createInsertCommand,
  createRemoveCommand,
  isAdoptAtCommand,
  isInsertCommand,
  isPositionCondition,
  isRemoveCommand,
  isSetCommand,
  isSnapshotCommand,
  ListPosition,
  ZERO,
  type AdoptAtCommand,
  type InsertCommand,
  type PositionCondition,
  type RemoveCommand,
  type SetCommand,
  type SnapshotCommand,
} from './commands.js';
import {
  $createOperation,
  $processServerResponse,
  $resolveOperation,
  $setValueQuietly,
  $update,
  type Operation,
  type ServerConnectionConfig,
} from './FullStackSignal.js';
import { ValueSignal } from './ValueSignal.js';

/**
 * A signal containing a list of values. Supports atomic updates to the list structure.
 * Each value in the list is accessed as a separate ValueSignal instance.
 */
export class ListSignal<T> extends CollectionSignal<Array<ValueSignal<T>>> {
  constructor(config: ServerConnectionConfig, id?: string) {
    super([], config, id);
  }

  /**
   * Inserts a value as the first entry in this list.
   * @param value - The value to insert
   * @returns An operation containing a signal for the inserted entry and the eventual result
   */
  insertFirst(value: T): Operation {
    return this.insertAt(value, ListPosition.first());
  }

  /**
   * Inserts a value as the last entry in this list.
   * @param value - The value to insert
   * @returns An operation containing a signal for the inserted entry and the eventual result
   */
  insertLast(value: T): Operation {
    return this.insertAt(value, ListPosition.last());
  }

  /**
   * Inserts a value at the given position in this list.
   * @param value - The value to insert
   * @param at - The insert position
   * @returns An operation containing a signal for the inserted entry and the eventual result
   */
  insertAt(value: T, at: ListPosition): Operation {
    const command = createInsertCommand<T>(ZERO, value, at);
    const promise = this[$update](command);
    return this[$createOperation]({ id: command.commandId, promise });
  }

  /**
   * Removes the given child from this list.
   * @param child - The child to remove
   * @returns An operation containing the eventual result
   */
  remove(child: ValueSignal<T>): Operation {
    const command = createRemoveCommand(child.id, ZERO);
    const promise = this[$update](command);
    return this[$createOperation]({ id: command.commandId, promise });
  }

  protected override [$processServerResponse](
    command: InsertCommand<T> | RemoveCommand | AdoptAtCommand | PositionCondition | SnapshotCommand | SetCommand<T>,
  ): void {
    // Check if the command has a targetNodeId and reroute it to the corresponding child
    if ((isSnapshotCommand(command) || isSetCommand(command)) && command.targetNodeId) {
      const targetChild = this.value.find((child) => child.id === command.targetNodeId);

      if (targetChild) {
        targetChild[$processServerResponse](command);
        return;
      }
    }

    if (isInsertCommand<T>(command)) {
      const valueSignal = new ValueSignal<T>(command.value, this.server.config, command.commandId, this);
      let insertIndex = this.value.length;
      const pos = command.position;
      if (pos.after === '' && pos.before == null) {
        insertIndex = 0;
      } else if (pos.after == null && pos.before === '') {
        insertIndex = this.value.length;
      } else if (typeof pos.after === 'string' && pos.after !== '') {
        const idx = this.value.findIndex((v) => v.id === pos.after);
        insertIndex = idx !== -1 ? idx + 1 : this.value.length;
      } else if (typeof pos.before === 'string' && pos.before !== '') {
        const idx = this.value.findIndex((v) => v.id === pos.before);
        insertIndex = idx !== -1 ? idx : this.value.length;
      }
      const newList = [...this.value.slice(0, insertIndex), valueSignal, ...this.value.slice(insertIndex)];
      this[$setValueQuietly](newList);
      this[$resolveOperation](command.commandId, undefined);
    } else if (isRemoveCommand(command)) {
      const removeIndex = this.value.findIndex((child) => child.id === command.targetNodeId);
      if (removeIndex !== -1) {
        const newList = [...this.value.slice(0, removeIndex), ...this.value.slice(removeIndex + 1)];
        this[$setValueQuietly](newList);
      }
      this[$resolveOperation](command.commandId, undefined);
    } else if (isAdoptAtCommand(command)) {
      const moveIndex = this.value.findIndex((child) => child.id === command.childId);
      if (moveIndex !== -1) {
        const [movedChild] = this.value.splice(moveIndex, 1);
        let newIndex = this.value.length;
        const pos = command.position;
        if (pos.after === '' && pos.before == null) {
          newIndex = 0;
        } else if (pos.after == null && pos.before === '') {
          newIndex = this.value.length;
        } else if (typeof pos.after === 'string' && pos.after !== '') {
          const idx = this.value.findIndex((v) => v.id === pos.after);
          newIndex = idx !== -1 ? idx + 1 : this.value.length;
        } else if (typeof pos.before === 'string' && pos.before !== '') {
          const idx = this.value.findIndex((v) => v.id === pos.before);
          newIndex = idx !== -1 ? idx : this.value.length;
        }
        this.value.splice(newIndex, 0, movedChild);
      }
      this[$resolveOperation](command.commandId, undefined);
    } else if (isPositionCondition(command)) {
      this[$resolveOperation](command.commandId, undefined);
    } else if (isSnapshotCommand(command)) {
      const { nodes } = command;
      const listNode = nodes[''];

      const childrenIds = listNode.listChildren;
      const valueSignals = childrenIds
        .map((childId) => {
          const childNode = nodes[childId];
          if ('value' in childNode) {
            return new ValueSignal<T>(childNode.value as T, this.server.config, childId, this);
          }
          return null;
        })
        .filter(Boolean) as Array<ValueSignal<T>>;

      this[$setValueQuietly](valueSignals);
      this[$resolveOperation](command.commandId, undefined);
    }
  }
}
